﻿namespace System.Dynamic
{
	using System.Collections;
	using System.Collections.Generic;
	using System.Linq;
	using System.Reflection;

	/// <summary>
	/// Represents a proxy that combines multiple objects into a single one.
	/// </summary>
	internal class CombinedObject : DynamicObject
	{
		public CombinedObject(params object[] objects)
		{
			if (objects == null)
				throw new ArgumentNullException("objects");

			if (!objects.Any())
				throw new ArgumentException("The provided collection of objects is empty.");

			this.objects = objects;
		}

		IEnumerable objects;


		public override bool TryGetMember(GetMemberBinder binder, out object result)
		{
			// read the value on the first matching object
			foreach (var o in objects)
			{
				// resolve property by name
				PropertyInfo pi = o.GetType().GetProperty(binder.Name, BindingFlags.Public | BindingFlags.Instance);

				if (pi != null)
				{
					// get property value
					result = pi.GetValue(o, null);
					return true;
				}

				// resolve field by name
				FieldInfo fi = o.GetType().GetField(binder.Name, BindingFlags.Public | BindingFlags.Instance);

				if (fi != null)
				{
					// get the value of the field
					result = fi.GetValue(o);
					return true;
				}
			}

			result = null;
			return false;
		}

		public override bool TrySetMember(SetMemberBinder binder, object value)
		{
			// set the value on the first matching object
			foreach (var o in objects)
			{
				// resolve property by name
				PropertyInfo pi = o.GetType().GetProperty(binder.Name, BindingFlags.Public | BindingFlags.Instance);

				if (pi != null)
				{
					pi.SetValue(o, value, null);
					return true;
				}

				// resolve field by name
				FieldInfo fi = o.GetType().GetField(binder.Name, BindingFlags.Public | BindingFlags.Instance);

				if (fi != null)
				{
					// set the value of the field
					fi.SetValue(o, value);
					return true;
				}
			}

			return false;
		}

		public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
		{
			// read the value on the first matching object
			foreach (var o in objects)
			{
				// resolve indexer property by name
				PropertyInfo pi = o.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance);

				if (pi != null)
				{
					// TODO: Avoid try-catch by passing index parameter types above, but we do not know the return type of the property
					try
					{
						// get property value
						result = pi.GetValue(o, indexes);
						return true;
					}
					catch { }
				}
			}

			result = null;
			return false;
		}

		public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
		{
			// set the value on the first matching object
			foreach (var o in objects)
			{
				// resolve indexer property by name
				PropertyInfo pi = o.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance);

				if (pi != null)
				{
					// TODO: Avoid try-catch by passing index parameter types above, but we do not know the return type of the property
					try
					{
						// set property value
						pi.SetValue(o, value, indexes);
						return true;
					}
					catch { }
				}
			}

			return false;
		}

		public override bool TryConvert(ConvertBinder binder, out object result)
		{
			// try return the first matching object
			foreach (var o in objects)
			{
				Type t = o.GetType();

				if (binder.Type.IsAssignableFrom(t))
				{
					result = o;
					return true;
				}
			}

			result = null;
			return false;
		}

		public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
		{
			// invoke the first matching object
			foreach (var o in objects)
			{
				// convert to delegate
				Delegate d = o as Delegate;

				if (d != null)
				{
					try
					{
						result = d.DynamicInvoke(args);
						return true;
					}
					catch { }
				}
			}

			result = null;
			return false;
		}

		public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
		{
			// invoke the member of the first matching object
			foreach (var o in objects)
			{
				// resolve method by name and argument types
				MethodInfo mi = o.GetType().GetMethod(binder.Name, args.Select(a => a.GetType()).ToArray());

				if (mi != null)
				{
					result = mi.Invoke(o, args);
					return true;
				}
			}

			result = null;
			return false;
		}

		public override IEnumerable<string> GetDynamicMemberNames()
		{
			return objects.Cast<object>().Select(o => o.GetType())
				.SelectMany(t => t.GetMembers(BindingFlags.Public | BindingFlags.Instance))
				.Select(m => m.Name).Distinct();
		}
	}
}
